package awk.etl.scripts;


import javax.tools.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 将java内容编译成class，放入内存中
 */
public class DynamicClass {
    private String javaSourceCode;
    private String fullClassName;
    private List<String> options = new ArrayList<>();

    /**
     * 由代码字符串编译出的字节码
     */
    public DynamicClass(String javaSourceCode, String fullClassName, List<String> options) {
        this.javaSourceCode = javaSourceCode;
        this.fullClassName = fullClassName;
        this.options = options;
    }

    /**
     * 基于字符串源码编译类
     *
     * @return
     */
    public Class<?> compilerFromSource() {
        // 获取Javac编译器对象
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
        // 获取文件管理器：负责管理类文件的输入输出
        StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnosticCollector, null, null);
        JavaFileManagerByteArray javaFileManagerByteArray = new JavaFileManagerByteArray(standardFileManager);
        // 生成编译任务
        JavaCompiler.CompilationTask task = compiler.getTask(null, javaFileManagerByteArray, diagnosticCollector, options, null, Collections.singletonList(new SourceJavaFileObject(fullClassName, javaSourceCode)));
        // 执行编译任务
        Boolean call = task.call();
        if (Boolean.TRUE.equals(call)) {
            try {
                byte[] classData = javaFileManagerByteArray.getBytesByClassName(fullClassName);
                DynamicClassLoader myClassLoader = new DynamicClassLoader(classData);
                return myClassLoader.loadClass(fullClassName);

                //换一种加载字节码的方式
//                Class<ClassLoader> classLoaderClass = ClassLoader.class;
//                Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
//                defineClass.setAccessible(true);
//                // 读取文件系统的 file 为 byte 数组
//                // 执行 defineClass 方法 返回 Class 对象
//                return (Class<?>) defineClass.invoke(Thread.currentThread().getContextClassLoader(), classData, 0, classData.length);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        }
        return null;
    }

    public Class<?> compilerFromJavaSource() {
        // 获取Javac编译器对象
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
        // 获取文件管理器：负责管理类文件的输入输出
        StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnosticCollector, null, null);
        JavaFileManagerMemory javaFileManagerMemory = new JavaFileManagerMemory(standardFileManager);
        // 生成编译任务
        JavaCompiler.CompilationTask task = compiler.getTask(null, standardFileManager, diagnosticCollector, null, null, Collections.singletonList( new SourceJavaFileObject(fullClassName, javaSourceCode)));
        // 执行编译任务
        Boolean call = task.call();
        if (Boolean.TRUE.equals(call)) {
            try {
                byte[] classData = javaFileManagerMemory.getJavaMemClass().getBytes();
                DynamicClassLoader myClassLoader = new DynamicClassLoader(classData);
                return myClassLoader.loadClass(fullClassName);

                //换一种加载字节码的方式
//                Class<ClassLoader> classLoaderClass = ClassLoader.class;
//                Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
//                defineClass.setAccessible(true);
//                // 读取文件系统的 file 为 byte 数组
//                // 执行 defineClass 方法 返回 Class 对象
//                return (Class<?>) defineClass.invoke(Thread.currentThread().getContextClassLoader(), classData, 0, classData.length);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        }
        return null;
    }


    /**
     * 基于源码文件加载类
     *
     * @param javaFilePath "/Users/yinqiqi/myworkspace/spring-lab8/all-demo/gorm6/etl-common/src/main/java/com/cloudwise/lcap/etl/etlcommon/scripts/Mycode.java";
     * @return
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    public static Class<?> compilerFromJavaFile(String javaFilePath) {
        try {
            // defineClass 为 ClassLoader 类的一个方法，用于加载类。但是这个方法是 protected 的，需要通过反射的方式获取这个方法的权限。如果不想使用反射的方式，也可以自定义ClassLoader,然后使用 super.findClass/defineClass() 来间接调用。
            //实际上，ClassLoader只把loadClass暴漏了，findClass/defineClass都是protected的
            Class<ClassLoader> classLoaderClass = ClassLoader.class;
            Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
            defineClass.setAccessible(true);
            // 读取文件系统的 file 为 byte 数组
            File file = new File(javaFilePath);
            byte[] bytes = new byte[(int) file.length()];
            FileInputStream fileInputStream = new FileInputStream(file);
            fileInputStream.read(bytes);
            /**
             * 执行 defineClass 方法 返回 Class 对象
             * ClassLoader#loadClass-->ClassLoader#findClass->ClassLoader#defineClass
             * loadClass的作用是从已加载的类缓存，父加载器等位置寻找(双亲委派机制)，在前面没有找到的情况下，执行findClass。
             * findClass的作用是根据URL指定的方式来加载类的字节码，可能会在本地系统，jar包或远程http服务器上读取字节码，然后将其交给defineClass。
             * defineClass的作用是处理前面传入的字节码，将其处理成真正的Java类。
             */
            return (Class<?>) defineClass.invoke(Thread.currentThread().getContextClassLoader(), bytes, 0, bytes.length);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | IOException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * -cp <目录和 zip/jar 文件的类搜索路径>
     * -classpath <目录和 zip/jar 文件的类搜索路径>
     * --class-path <目录和 zip/jar 文件的类搜索路径>
     * 使用 ; 分隔的, 用于搜索类文件的目录, JAR 档案
     * 和 ZIP 档案列表。
     * -p <模块路径>
     * --module-path <模块路径>...
     * 用 ; 分隔的目录列表, 每个目录
     * 都是一个包含模块的目录。
     * --upgrade-module-path <模块路径>...
     * 用 ; 分隔的目录列表, 每个目录
     * 都是一个包含模块的目录, 这些模块
     * 用于替换运行时映像中的可升级模块
     * --add-modules <模块名称>[,<模块名称>...]
     * 除了初始模块之外要解析的根模块。
     * <模块名称> 还可以为 ALL-DEFAULT, ALL-SYSTEM,
     * ALL-MODULE-PATH.
     * --list-modules
     * 列出可观察模块并退出
     * -d <module name>
     * --describe-module <模块名称>
     * 描述模块并退出
     * --dry-run     创建 VM 并加载主类, 但不执行 main 方法。
     * 此 --dry-run 选项对于验证诸如
     * 模块系统配置这样的命令行选项可能非常有用。
     * --validate-modules
     * 验证所有模块并退出
     * --validate-modules 选项对于查找
     * 模块路径中模块的冲突及其他错误可能非常有用。
     * -D<名称>=<值>
     * 设置系统属性
     * -verbose:[class|module|gc|jni]
     * 为给定子系统启用详细输出
     * -version      将产品版本输出到错误流并退出
     * --version     将产品版本输出到输出流并退出
     * -showversion  将产品版本输出到错误流并继续
     * --show-version
     * 将产品版本输出到输出流并继续
     * --show-module-resolution
     * 在启动过程中显示模块解析输出
     * -? -h -help
     * 将此帮助消息输出到错误流
     * --help        将此帮助消息输出到输出流
     * -X            将额外选项的帮助输出到错误流
     * --help-extra  将额外选项的帮助输出到输出流
     * -ea[:<程序包名称>...|:<类名>]
     * -enableassertions[:<程序包名称>...|:<类名>]
     * 按指定的粒度启用断言
     * -da[:<程序包名称>...|:<类名>]
     * -disableassertions[:<程序包名称>...|:<类名>]
     * 按指定的粒度禁用断言
     * -esa | -enablesystemassertions
     * 启用系统断言
     * -dsa | -disablesystemassertions
     * 禁用系统断言
     * -agentlib:<库名>[=<选项>]
     * 加载本机代理库 <库名>, 例如 -agentlib:jdwp
     * 另请参阅 -agentlib:jdwp=help
     * -agentpath:<路径名>[=<选项>]
     * 按完整路径名加载本机代理库
     * -javaagent:<jar 路径>[=<选项>]
     * 加载 Java 编程语言代理, 请参阅 java.lang.instrument
     * -splash:<图像路径>
     * 使用指定的图像显示启动屏幕
     * 自动支持和使用 HiDPI 缩放图像
     * (如果可用)。应始终将未缩放的图像文件名 (例如, image.ext)
     * 作为参数传递给 -splash 选项。
     * 将自动选取提供的最合适的缩放
     * 图像。
     * 有关详细信息, 请参阅 SplashScreen API 文档
     * <p>
     * 一个或多个包含选项的参数文件
     * -disable-@files
     * 阻止进一步扩展参数文件
     * --enable-preview
     * 允许类依赖于此发行版的预览功能
     * 要为长选项指定参数, 可以使用 --<名称>=<值> 或
     * --<名称> <值>。
     */
    public DynamicClass addClasspath(String classpath) {
        //编译配置classpath
        options.add("-classpath");
        options.add(classpath);
        return this;
    }
}
